package com.panopset.flywheel;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Stack;
import java.util.StringTokenizer;

import com.panopset.Dump;
import com.panopset.MapProvider;
import com.panopset.StringLineSupplier;
import com.panopset.UniqueKey;
import com.panopset.Util;
import com.panopset.compat.Logz;
import com.panopset.compat.Strings;
import com.panopset.io.FileProcessor;
import com.panopset.io.FileProps;
import com.panopset.io.Filez;
import com.panopset.io.StreamProcessor;

/**
 * Panopset Flywheel. <h3>Commands</h3> There are 7 commands that you may use in
 * a Flywheel template. Commands have a default syntax that starts with
 * <b>${&#064;</b>, followed by the letter associated with the command, followed
 * by a space, followed by a parameter, followed by the default closing syntax
 * of <b>}</b>. <h5>f - File</h5>
 * 
 * <pre>
 * ${&#064f somefile.txt}
 * </pre>
 * 
 * Output to the specified file, until the matching ${&#064q} is found. <h5>p -
 * Push</h5>
 * 
 * <pre>
 * 
 * ${&#064p variableName}
 * </pre>
 * 
 * Everything following this command is pushed into a String buffer, until a q
 * command is reached. <h5>l - List</h5>
 * 
 * <pre>
 * ${&#064l someListFile.txt} or ${&#064l someFile.txt&tilde;,}
 * </pre>
 * 
 * Read the given file, and for each line execute the template from this list
 * command up until its matching q command. If no <b>token</b> is defined as a
 * variable, then each line will be stored in variable <b>1</b>. If there is a
 * token defined after a ~, the line will be split by that token, and stored in
 * variables named after integers, in order.
 * 
 * <h5>q - Quit</h5>
 * 
 * <pre>
 * ${&#064q}
 * </pre>
 * <ul>
 * <li>If the q command follows a p command, the String buffer is defined as the
 * variable name that was provided in the p command.</li>
 * <li>If the q command follows an l command, the String buffer is read for the
 * each item in the list.</li>
 * </ul>
 * <h5>t - Template</h5>
 * 
 * <pre>
 * ${&#064t someTemplateFile.txt}
 * </pre>
 * 
 * Continue execution using the supplied template script file. <h5>Variable</h5>
 * There is no command associated with a variable, so you drop the <b>&#064</b>
 * directive indicator, and then you specify a variable just as you would in any
 * ant script or unix shell. The variable must have been defined either in a map
 * provided to the script through Script.Builder.mergeMap, or a Push command.
 * 
 * <pre>
 * ${variableName}
 * </pre>
 * 
 * <h5>e - Execute</h5> Excecute any static method. Parameters may be variable
 * names only. Example:
 * 
 * <pre>
 * 
 * ${&#064p name}panopset${q}
 * 
 * ${&#064e com.panopset.compat.new Stringz().capitalize(name)}
 * 
 * </pre>
 * 
 * The above script will output:
 * 
 * <pre>
 * 
 * Panopset
 * 
 * </pre>
 * 
 * @author Karl Dinwiddie
 */
public final class Flywheel implements MapProvider {

	/**
	 * Target directory.
	 */
	private File targetDirectory;

	/**
	 * @return Target directory.
	 */
	public File getTargetDirectory() {
		return targetDirectory;
	}

	/**
	 * File.
	 */
	private File file;

	/**
	 * String line supplier.
	 */
	private final StringLineSupplier sls;

	/**
	 * Currently resolving command.
	 */
	private Command resolvingCommand;

	/**
	 * Used for debugging.
	 * 
	 * @return Currently resolving command.
	 */
	public Command getResolvingCommand() {
		return resolvingCommand;
	}

	/**
	 * Writer for this Flywheel.
	 */
	private StringWriter pwriter;

	/**
	 * Map stack.
	 */
	private final Stack<NamedMap<String, String>> mapStack

	= new Stack<NamedMap<String, String>>();

	/**
	 * @return Map stack.
	 */
	Stack<NamedMap<String, String>> getMapStack() {
		return mapStack;
	}

	/**
	 * @return All map values.
	 */
	public Map<String, String> getAllValues() {
		Map<String, String> rtn = new HashMap<String, String>();
		for (NamedMap<String, String> nm : getMapStack()) {
			for (Entry<String, String> e : nm.getMap().entrySet()) {
				rtn.put(e.getKey(), e.getValue());
			}
		}
		return rtn;
	}

	/**
	 * Current command file.
	 */
	private CommandFile currentCommandFile;

	/**
	 * @return Current command file.
	 */
	CommandFile getCurrentCommandFile() {
		return currentCommandFile;
	}

	/**
	 * Set current command file.
	 * 
	 * @param commandFile
	 *            New current command file.
	 */
	void setCurrentCommandFile(final CommandFile commandFile) {
		currentCommandFile = commandFile;
	}

	/**
	 * Replacements.
	 */
	private List<String[]> replacements;

	/**
	 * Registered objects.
	 */
	private Map<String, Object> registeredObjs;

	/**
	 * Report.
	 */
	private Report report;

	/**
	 * Set report.
	 * 
	 * @param newReport
	 *            New report.
	 */
	void setReport(final Report newReport) {
		report = newReport;
	}

	/**
	 * @param args
	 *            <pre>
	 *     [0] = script file name, args[1] = target directory name.
	 *       or
	 *     [0] = properties file name where these properties are required:
	 *            com.panopset.flywheel.script=&lt;your script file name&gt;
	 * com.panopset.flywheel.target=&lt;your target file name&gt;
	 * </pre>
	 */
	public static void main(final String... args) {
		try {
			if (args == null || args.length == 0 || args.length > 2) {
				System.out.println("Params are script and target directory.");
				// new Gui().setVisible(true);
			} else {
				Flywheel flywheel = null;
				if (args.length == 1) {
					flywheel = new Flywheel.Builder().properties(
							new File(args[0])).construct();
				} else {
					flywheel = new Flywheel.Builder().file(new File(args[0]))
							.targetDirectory(new File(args[1])).construct();
				}
				flywheel.exec();
			}
		} catch (RuntimeException ex) {
			Logz.log(ex);
		}
	}

	/**
	 * Copy package resource.
	 * 
	 * @param resourcePath
	 *            Java source path resource.
	 * @param targetPath
	 *            Target path, relative to Flywheel target directory.
	 * @throws IOException
	 *             IO Exception.
	 */
	public void copyPackageResource(final String resourcePath,
			final String targetPath) throws IOException {
		try {
			new StreamProcessor().copyLibraryResource(resourcePath,
					new Filez().getCanonicalPath(targetDirectory) + "/"
							+ targetPath);
		} catch (IOException ex) {
			StringWriter sw = new StringWriter();

			sw.append("baseDirectoryPath: " + baseDirectoryPath);
			sw.append(new Strings().getEol());
			sw.append("Template file: " + new Filez().getCanonicalPath(file));
			sw.append(new Strings().getEol());
			sw.append("targetDirectory: " + targetDirectory);
			sw.append(new Strings().getEol());
			sw.append("resourcePath: " + resourcePath);
			sw.append(new Strings().getEol());
			sw.append("targetPath: " + targetPath);
			sw.append(new Strings().getEol());
			if (template != null && template.getStringLineSupplier() != null) {
				sw.append("executing from: "
						+ template.getStringLineSupplier().getFullName());
				sw.append(new Strings().getEol());
				sw.append("Line: ");
				sw.append("" + template.getStringLineSupplier().getLine());
				sw.append(new Strings().getEol());
			}
			Logz.log(sw.toString());
			throw (ex);
		}
	}

	/**
	 * Flywheel constructor.
	 * 
	 * @param stringLineSupplier
	 *            line supplier.
	 */
	private Flywheel(final StringLineSupplier stringLineSupplier) {
		sls = stringLineSupplier;
		this.mapStack.push(new NamedMap<String, String>(UniqueKey
				.generateStackHashKey()));
		this.mapStack.peek().put(ReservedWords.FILE, sls.getName());
	}

	/**
	 * Is target directory valid.
	 * 
	 * @return true if a valid, writable target directory has been specified.
	 */
	public boolean isTargetDirectoryValid() {
		if (targetDirectory != null && !targetDirectory.exists()) {
			new Filez().mkdirs(targetDirectory);
		}
		return targetDirectory != null && targetDirectory.isDirectory()
				&& targetDirectory.exists() && targetDirectory.canWrite();
	}

	/**
	 * This method should only be used when a writer was specified in the
	 * builder. In other words, when you are using the templates to generate a
	 * text String, instead of files, then you would use this method to get the
	 * output.
	 * 
	 * @return StringWriter holds script output.
	 */
	public StringWriter getWriter() {
		if (pwriter == null) {
			pwriter = new StringWriter();
		}
		return pwriter;
	}

	/**
	 * @param writer
	 *            Writer.
	 */
	public void setWriter(final StringWriter writer) {
		pwriter = writer;
	}

	/**
	 * Put map value, that will be available to future v commands.
	 * 
	 * @param key
	 *            Variable name.
	 * @param value
	 *            Variable value.
	 */
	public void put(final String key, final String value) {
		mapStack.peek().put(key, value);
	}

	/**
	 * Load properties.
	 * 
	 * @param relativePath
	 *            Path is relative to the parent of the main Flywheel script
	 *            file.
	 * @return empty String.
	 */
	public String loadProperties(final String relativePath) {
		File f = new File(getBaseDirectoryPath() + "/" + relativePath);
		Properties p = new Properties();
		if (!f.exists()) {
			try {
				new StreamProcessor().copyLibraryResource(relativePath, f);
			} catch (IOException ex) {
				Logz.log("relativePath: " + relativePath);
			}
		}
		FileProps.loadProperties(p, f);
		for (Object k : p.keySet()) {
			put(k.toString(), p.getProperty(k.toString()).toString());
		}
		return "";
	}

	/**
	 * Suppress replacements.
	 * 
	 * <b>Usage</b>
	 * 
	 * <pre>
	 * ${&#064p foo}bar{&#064q}
	 * ${&#064e script.suppressReplacements(true)}
	 * A: ${&#064v foo}
	 * ${&#064e script.suppressReplacements(false)}
	 * B: ${&#064v foo}
	 * </pre>
	 * 
	 * <b>Output</b>
	 * 
	 * <pre>
	 * 
	 * A:
	 * B: bar
	 * 
	 * </pre>
	 * 
	 * @param value
	 *            If true, all v commands will be not output anything.
	 */
	public void suppressReplacements(final String value) {
		if (value == null) {
			control.setReplacementsSuppressed(false);
			return;
		}
		control.setReplacementsSuppressed(Boolean.parseBoolean(value));
	}

	/**
	 * Get map value.
	 * 
	 * @param key
	 *            Variable name.
	 * @return value or null if key not found.
	 */
	@Override
	public String get(final String key) {
		if (mapStack.isEmpty()) {
			stop("mapStack empty, this should be an impossible condition.");
			return null;
		}
		Stack<NamedMap<String, String>> stack

		= new Stack<NamedMap<String, String>>();
		for (NamedMap<String, String> item : mapStack) {
			stack.push(item);
		}
		NamedMap<String, String> m = stack.pop();
		while (true) {
			String rtn = m.get(key);
			if (rtn != null) {
				return rtn;
			}
			if (stack.isEmpty()) {
				return null;
			}
			m = stack.pop();
		}
	}

	/**
	 * Merge a map into the script map. Existing values are overwritten.
	 * 
	 * @param map
	 *            Map to merge into the script map.
	 */
	public void mergeMap(final Map<String, String> map) {
		mapStack.push(

		new NamedMap<String, String>(UniqueKey.generateStackHashKey(), map));
	}

	/**
	 * Report source file.
	 * 
	 * @param sourceFile
	 *            Source file.
	 */
	void reportSourceFile(final SourceFile sourceFile) {
		if (report != null) {
			report.addSourceFile(sourceFile);
		}
	}

	/**
	 * Execute this script.
	 * 
	 * @return results, for unit tests and server side operations.
	 */
	public String exec() {
		String rtn = "";
		new Logz().info("Base directory: " + getBaseDirectoryPath());
		if (file != null) {
			new Logz().info("Executing: "
					+ new Filez().getCanonicalPath(file));
		}
		try {
			getTemplate().output(getWriter());
			rtn = getWriter().toString();
			if (report == null) {
				return rtn;
			}
			if (file != null) {
				reportSourceFile(new SourceFile(file, getBaseDirectoryPath()));
			}
			report.exec();
		} catch (Throwable t) {
			Logz.log(t);
			stop(t.getMessage());
		}
		return rtn;
	}

	/**
	 * Template.
	 */
	private Template template;

	/**
	 * @return Template.
	 */
	public Template getTemplate() {
		if (template == null) {
			template = new Template(this, sls);
		}
		return template;
	}

	/**
	 * Called if something unexpected happens. Diagnostic messages are logged,
	 * in addition to the message.
	 * 
	 * @param message
	 *            Log this message
	 */
	public void stop(final String message) {
		Logz.log(message);
		Logz.log("Unexpected stop, dumping top map.");
		if (template != null) {
			Logz.log(template.getRelativePath());
		}
		Logz.log(new Dump(mapStack.peek()).toString());
		if (resolvingCommand != null) {
			Logz.log("Stopped while executing line "
					+ resolvingCommand.getLineNumber()
					+ resolvingCommand.toString());
		}
		control.stop();
	}

	/**
	 * Used by debuggers.
	 * 
	 * @return true if the stop method was called for any reason, usually some
	 *         error condition.
	 */
	public boolean isStopped() {
		return control.isStopped();
	}

	/**
	 * Base directory path.
	 */
	private String baseDirectoryPath;

	/**
	 * Get the directory in which the script top template file resides. This is
	 * used as a relative path base for all subsequent template files created
	 * with the template command: <b>${&#064;t templateRelativePathName}</b>.
	 * 
	 * @return Full path of parent directory of the top script template file.
	 */
	public String getBaseDirectoryPath() {
		if (baseDirectoryPath == null) {
			baseDirectoryPath = System.getProperty("user.home");
		}
		return baseDirectoryPath;
	}

	/**
	 * Set replacements vector.
	 * 
	 * @param replacementsList
	 *            Replacements vector.
	 */
	void setReplacements(final List<String[]> replacementsList) {
		this.replacements = replacementsList;
	}

	/**
	 * @return Replacements vector.
	 */
	protected List<String[]> getReplacements() {
		if (replacements == null) {
			replacements = new ArrayList<String[]>();
		}
		return replacements;
	}

	/**
	 * Append simple file name to getBaseDirectoryPath.
	 * 
	 * @param simpleFileName
	 *            Simple file name (no path).
	 * @return Full relative path of simpleFileName
	 */
	public String getBaseRelativePath(final String simpleFileName) {
		return getBaseDirectoryPath() + "/" + simpleFileName;
	}

	/**
	 * Flywheel builder.
	 */
	public static final class Builder {

		/**
		 * Is output enabled.
		 */
		private boolean isOutputEnabled = true;

		/**
		 * Writer.
		 */
		private StringWriter writer;

		/**
		 * Optional Flywheel script report.
		 */
		private Report pReport;

		/**
		 * Target directory.
		 */
		private File targetDirectory;

		/**
		 * Template array.
		 */
		private String[] array;

		/**
		 * String line supplier.
		 */
		private StringLineSupplier sls;

		/**
		 * Template file.
		 */
		private File file;

		/**
		 * Base directory path.
		 */
		private String baseDirectoryPath;

		/**
		 * Variables map.
		 */
		private Map<String, String> map = new HashMap<String, String>();

		/**
		 * Registered objects for static methods that return String values.
		 */
		private Map<String, Object> registeredObjects

		= new HashMap<String, Object>();

		/**
		 * @return Optional Flywheel script report.
		 */
		Report getReport() {
			if (pReport == null) {
				pReport = new Report();
			}
			return pReport;
		}

		/**
		 * Create a script. Example usage:
		 * 
		 * <pre>
		 * new Script.Builder()
		 * 
		 * .targetDirectory(&quot;publish/site&quot;).scriptFile(
		 * 
		 * &quot;templates/index.txt&quot;).properties(
		 * 
		 * &quot;my.properties&quot;) // Script
		 * 		// variables.
		 * 		.constructScript().exec();
		 * </pre>
		 * 
		 * @return Panopset Flywheel Script object.
		 */
		public Flywheel construct() {
			String scriptFileName = this.map.get(ReservedWords.SCRIPT);
			String targetDirName = this.map.get(ReservedWords.TARGET);
			if (Strings.isPopulated(scriptFileName)) {
				this.file = new File(scriptFileName);
			}
			if (this.file == null) {
				if (this.array == null) {
					this.file = new File("com.panopset.flywheel.EmptyFile.txt");
					this.sls = new StringLineSupplier(this.file);
				} else {
					this.sls = new StringLineSupplier(this.array);
				}
			} else {
				this.sls = new StringLineSupplier(this.file);
			}
			Flywheel flywheel = new Flywheel(sls);
			if (this.baseDirectoryPath != null) {
				flywheel.baseDirectoryPath = this.baseDirectoryPath;
			}
			if (this.file != null && flywheel.baseDirectoryPath == null) {
				flywheel.baseDirectoryPath = new Filez()
						.getParentDirectory(this.file);
			}
			flywheel.mergeMap(this.map);
			flywheel.setReplacements(replacements);
			if (Strings.isPopulated(targetDirName)) {
				flywheel.targetDirectory = new File(targetDirName);
			}
			if (flywheel.targetDirectory == null) {
				flywheel.targetDirectory = getTargetDirectory();
			}
			flywheel.put(ReservedWords.TARGET,
					new Filez().getCanonicalPath(getTargetDirectory()));
			flywheel.setWriter(writer);
			registeredObjects.put(ReservedWords.FLYWHEEL, flywheel);
			flywheel.setRegisteredObjects(registeredObjects);
			if (this.file != null) {
				flywheel.file = this.file;
			}
			if (pReport != null) {
				pReport.setFlywheel(flywheel);
			}
			flywheel.setOutputEnabled(isOutputEnabled);
			return flywheel;
		}

		/**
		 * Set target directory. This is where your output is going. Directory
		 * structures will be created as needed, and <b>existing files are wiped
		 * without warning</b>.
		 * 
		 * @param newTargetDirectory
		 *            All output files specified by the ${&#064f fileName}
		 *            command are relative paths based here.
		 * @return Builder.
		 */
		public Builder targetDirectory(final File newTargetDirectory) {
			targetDirectory = newTargetDirectory;
			return this;
		}

		/**
		 * Set script file.
		 * 
		 * @param scriptFile
		 *            Specify a single controlling script file.
		 * @return Builder.
		 */
		public Builder file(final File scriptFile) {
			file = scriptFile;
			return this;
		}

		/**
		 * You do not need to specify this if you are specifying a File in
		 * Flywheel.Builder.file, as the parent directory of the main template
		 * file would be used. This method would rarely be needed, but is
		 * available for flexibility.
		 * 
		 * @param path
		 *            Base directory path.
		 * @return Builder.
		 */
		public Builder baseDirectoryPath(final String path) {
			baseDirectoryPath = path;
			return this;
		}

		/**
		 * Place html reports on your template structure in this directory. Use
		 * reports(File reportsDirectory) if this is for the server side.
		 * 
		 * @param reportsRelativePath
		 *            Relative path to targetDirectory
		 * @return Builder.
		 */
		public Builder reports(final String reportsRelativePath) {
			getReport().setReportsRelativePath(reportsRelativePath);
			return this;
		}

		/**
		 * If you want reports placed in a directory outside the target
		 * directory, or if there is no target directory, as would be the case
		 * on the server side, you may specify a reports directory here. An
		 * error will be thrown if the reportsDirectory file exists and is not a
		 * directory.
		 * 
		 * @param newReportsDirectory
		 *            Reports directory.
		 * @return Builder.
		 * @throws IOException
		 *             IO Exception.
		 */
		public Builder reports(final File newReportsDirectory)
				throws IOException {
			if (newReportsDirectory.exists() && newReportsDirectory.isFile()) {
				throw new IOException("Will not overwrite file "
						+ new Filez().getCanonicalPath(newReportsDirectory));
			}
			getReport().setReportDirectory(newReportsDirectory);
			return this;
		}

		/**
		 * If you supply a String array here, do not use the <b>file</b>
		 * Flywheel.Builder method.
		 * 
		 * @param newStringArray
		 *            Script commands in String array.
		 * @return Builder.
		 */
		public Builder input(final String[] newStringArray) {
			array = newStringArray;
			return this;
		}

		/**
		 * If you supply a List of Strings here, do not use the <b>file</b>
		 * Flywheel.Builder method.
		 * 
		 * @param newInput
		 *            Script commands in String List.
		 * @return Builder.
		 */
		public Builder input(final List<String> newInput) {
			final String[] sa = new String[newInput.size()];
			int i = 0;
			for (String s : newInput) {
				sa[i++] = s;
			}
			return this.input(sa);
		}

		/**
		 * Defaults to &quot;temp&quot;.
		 * 
		 * @return target directory File.
		 */
		private File getTargetDirectory() {
			if (targetDirectory == null) {
				return new File("temp");
			}
			return targetDirectory;
		}

		/**
		 * You may pre-define variables by supplying a map. Please remember to
		 * stay away from the the numbers, as they are used in lists.
		 * 
		 * @param additionalMap
		 *            Additional map. In the case of duplicate keys, the most
		 *            recently added map takes precedence.
		 * @return Builder.
		 */
		public Builder map(final Map<String, String> additionalMap) {
			Util.copyMap(additionalMap, map);
			return this;
		}

		/**
		 * Add a key value pair to the map.
		 * 
		 * @param key
		 *            Variable command name.
		 * @param value
		 *            Variable value.
		 * @return Builder.
		 */
		public Builder map(final String key, final String value) {
			this.map.put(key, value);
			return this;
		}

		/**
		 * Specify list line tokens, default is none ("").
		 * 
		 * @param tokens
		 *            List line tokens.
		 * @return Builder.
		 */
		public Builder tokens(final String tokens) {
			map.put(ReservedWords.TOKENS, tokens);
			return this;
		}

		/**
		 * Register any instantiated objects you want available to the Execute
		 * command here. You will be able to use any method of those objects
		 * that accept 0 or more String parameters, and that return a single
		 * String result. Recommended, but not required, to qualify objects with
		 * their path names. com.panopset.flywheel.Flywheel is registered
		 * automatically.
		 * 
		 * @param key
		 *            Name object will be known by, in Execute commands.
		 * @param obj
		 *            Instance Execute commands will invoke methods on.
		 * @return Builder.
		 */
		public Builder registerObject(final String key, final Object obj) {
			registeredObjects.put(key, obj);
			return this;
		}

		/**
		 * Load properties file into pre-defined variables.
		 * 
		 * @see Flywheel.Builder#map
		 * @param propertiesFile
		 *            Properties file.
		 * @return Builder.
		 */
		public Builder properties(final File propertiesFile) {
			if (propertiesFile != null && propertiesFile.exists()) {
				return map(Util.loadPropsFromFile(propertiesFile));
			}
			if (propertiesFile != null) {
				Logz.log(new Filez().getCanonicalPath(propertiesFile)
						+ " does not exist, skipping.");
			}
			return this;
		}

		/**
		 * @param properties
		 *            Properties file.
		 * @return Builder.
		 */
		public Builder properties(final Properties properties) {
			return map(Util.loadMapFromProperties(properties));
		}

		/**
		 * Define replacements in a file specified by the replacements
		 * parameter. First line of that file contains a single character that
		 * is the separator for subsequent lines that define replacements.
		 * 
		 * @param replacementsFile
		 *            file.
		 * @return Builder.
		 */
		public Builder replacements(final File replacementsFile) {
			final List<String[]> v = new ArrayList<String[]>();
			new FileProcessor.Builder()
					.fileAndListener(replacementsFile,
							new FileProcessor.Listener() {

								private String separator;

								@Override
								public void finish(

								final FileProcessor processor) {
									// nothing to do here
								}

								@Override
								public void process(
										final FileProcessor processor,
										final String[] s) {
									if (separator == null) {
										separator = s[0];
									} else {
										StringTokenizer st

										= new StringTokenizer(s[0], separator);
										v.add(new String[] { st.nextToken(),
												st.nextToken() });
									}
								}
							}).construct().exec();
			for (String[] r : v) {
				replacement(r);
			}
			return this;
		}

		/**
		 * Add a global replacement. Replacements are performed in the order
		 * added.
		 * 
		 * @param s
		 *            s[0] = from s[1] = to.
		 * @return Builder.
		 */
		public Builder replacement(final String[] s) {
			return replacement(s[0], s[1]);
		}

		/**
		 * Add a global replacement. Replacements are performed in the order
		 * added.
		 * 
		 * @param from
		 *            String to replace with the to String.
		 * @param to
		 *            String to put in place of the from String.
		 * @return Builder.
		 */
		public Builder replacement(final String from, final String to) {
			getReplacements().add(new String[] { from, to });
			return this;
		}

		/**
		 * Replacements map.
		 */
		private List<String[]> replacements;

		/**
		 * @return replacements map.
		 */
		private List<String[]> getReplacements() {
			if (replacements == null) {
				replacements = new ArrayList<String[]>();
			}
			return replacements;
		}

		/**
		 * If you specify a writer, you need not specify a targetDirectory, as
		 * it would be meaningless, and any File commands are ignored, as you
		 * are intending your output to be a StringWriter.
		 * 
		 * @param newWriter
		 *            StringWriter to write output to.
		 * @return Builder.
		 */
		public Builder writer(final StringWriter newWriter) {
			writer = newWriter;
			isOutputEnabled = false;
			return this;
		}

		/**
		 * If you want a link back to your site at the top of your reports,
		 * supply a title and link here.
		 * 
		 * @param title
		 *            Usually "home".
		 * @param href
		 *            Relative or absolute path. For example ("../index.html").
		 *            Don't forget the correct number of "../" entries, based
		 *            upon what you had put in the reports path.
		 * @see com.panopset.flywheel.Flywheel.Builder#preports
		 * @return Builder.
		 */
		public Builder reportsHomeLink(final String title, final String href) {
			getReport().setReportsHomeLinkTitle(title);
			getReport().setReportsHomeLinkHref(href);
			return this;
		}
	}

	/**
	 * Is replacements repressed.
	 * 
	 * @return true if replacements suppressed flag is set.
	 */
	public boolean isReplacementsSuppressed() {
		return getControl().isReplacementsSuppressed();
	}

	/**
	 * @param registeredObjects
	 *            Registered objects.
	 */
	public void setRegisteredObjects(

	final Map<String, Object> registeredObjects) {
		for (Entry<String, Object> e : registeredObjects.entrySet()) {
			getRegisteredObjects().put(e.getKey(), e.getValue());
		}
	}

	/**
	 * @return Registered objects for execute commands.
	 */
	public Map<String, Object> getRegisteredObjects() {
		if (registeredObjs == null) {
			registeredObjs = new HashMap<String, Object>();
		}
		return registeredObjs;
	}

	/**
	 * Flywheel control.
	 */
	private final Control control = new Control();

	/**
	 * Get control.
	 * 
	 * @return Flywheel Control.
	 */
	public Control getControl() {
		return control;
	}

	/**
	 * Is output to files enabled.
	 */
	private boolean isOutputEnabled = true;

	/**
	 * @return True iff output to files is enabled, default is true.
	 */
	public boolean isOutputEnabled() {
		return isOutputEnabled;
	}

	/**
	 * Set whether or not Flywheel will write output to text files. Default is
	 * true, you might want to set to false if all you are doing is String
	 * manipulation in memory.
	 * 
	 * @param value
	 *            Value to set.
	 */
	public void setOutputEnabled(final boolean value) {
		this.isOutputEnabled = value;
	}
}
